﻿// ***********************************************************************
// Provided for Informational Purposes Only
//
// Apache 2.0 License
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 
//
// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
// ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY 
// IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR 
// PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT.
//
// See the Apache Version 2.0 License for specific language governing
// permissions and limitations under the License.
// ***********************************************************************
// Assembly  : ContractMeow.Api.WinRT
// File      : ClientBase.cs
//
// ***********************************************************************
using ContractMeow.Api.Dtos;
using ContractMeow.Api.Exceptions;
using ContractMeow.Api.Net;
using ContractMeow.WinRT.Serialization;
using System;
using System.Diagnostics;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Windows.Storage;

namespace ContractMeow.Api.Clients
{
	/// <summary>
	/// Class ClientBase
	/// </summary>
	public abstract class ClientBase : IDisposable
	{
		private readonly ICookieStore _cookieStore;

		private bool _disposed;

		#region Protected Properties

		/// <summary>
		/// Gets or sets the query string serializer.
		/// </summary>
		/// <value>The query string serializer.</value>
		protected IQueryStringSerializer QueryStringSerializer { get; set; }

		/// <summary>
		/// Gets or sets the serializer.
		/// </summary>
		/// <value>The serializer.</value>
		protected ISerializer Serializer { get; set; }

		/// <summary>
		/// Gets or sets the deserializer.
		/// </summary>
		/// <value>The deserializer.</value>
		protected IDeserializer Deserializer { get; set; }

		/// <summary>
		/// Gets or sets the response error handler.
		/// </summary>
		/// <value>The response error handler.</value>
		protected IResponseErrorHandler ResponseErrorHandler { get; set; }

		#endregion

		#region Public Properties

		/// <summary>
		/// Gets the configuration.
		/// </summary>
		/// <value>The configuration.</value>
		public IClientConfiguration Configuration { get; internal set; }

		/// <summary>
		/// Gets the proxy.
		/// </summary>
		/// <value>The proxy.</value>
		public IWebProxy Proxy { get; internal set; }

		private HttpClientHandler _httpClientHandler;
		private HttpClient _httpClient;

		#endregion

		#region Ctors

		/// <summary>
		/// Initializes a new instance of the <see cref="ClientBase" /> class.
		/// </summary>
		/// <param name="configuration">The configuration.</param>
		/// <param name="queryStringSerializer">The query string serializer.</param>
		/// <param name="serializer">The serializer.</param>
		/// <param name="deserializer">The deserializer.</param>
		/// <param name="responseErrorHandler">The response error handler.</param>
		/// <param name="proxy">The proxy.</param>
		/// <exception cref="System.ArgumentNullException">configuration</exception>
		protected ClientBase(IClientConfiguration configuration,
							 IQueryStringSerializer queryStringSerializer,
							 ISerializer serializer,
							 IDeserializer deserializer,
							 IResponseErrorHandler responseErrorHandler,
							 ICookieStore cookieStore,
							 IWebProxy proxy = null)
		{

			_cookieStore = cookieStore;

			if (configuration == null)
				throw new ArgumentNullException("configuration");
			if (queryStringSerializer == null)
				throw new ArgumentNullException("queryStringSerializer");
			Configuration = configuration;
			QueryStringSerializer = queryStringSerializer;
			Serializer = serializer;
			Deserializer = deserializer;
			ResponseErrorHandler = responseErrorHandler;
			Proxy = proxy;
			_httpClientHandler = new HttpClientHandler
			{
				AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
				UseProxy = Proxy != null,
				Proxy = Proxy,
				CookieContainer = _cookieStore != null ? _cookieStore.CookieContainer : null,
				UseCookies = _cookieStore != null,
			};
			_httpClient = new HttpClient(_httpClientHandler);
			_httpClient.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue { MustRevalidate = true };
			_httpClient.Timeout = Configuration.Timeout;
			_httpClient.DefaultRequestHeaders.Add(HttpRequestHeader.UserAgent.ToString(), Configuration.UserAgent);
		}

		#endregion

		#region Wrappers around ExecuteAsync for different HTTP request types

		/// <summary>
		/// Gets asynchronously.
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="uri">The URI.</param>
		/// <param name="principal">The principal.</param>
		/// <param name="parameters">The parameters.</param>
		/// <returns>Task{``0}.</returns>
		protected Task<IBaseEnvelope<T>> GetAsync<T>(String uri, Principal principal = null, Object parameters = null)
			where T : class
		{
			IAuthenticator authenticator = principal != null
											   ? GetDefaultAuthenticator(principal.AccessToken)
											   : null;
			return ExecuteAsync<T>(HttpMethod.Get, uri, parameters, authenticator: authenticator);
		}

		/// <summary>
		/// Posts asynchronously.
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="uri">The URI.</param>
		/// <param name="principal">The principal.</param>
		/// <param name="parameters">The parameters.</param>
		/// <returns>Task{``0}.</returns>
		protected Task<IBaseEnvelope<T>> PostAsync<T>(String uri, Principal principal = null, Object parameters = null, IStorageItem uploadFile = null)
			where T : class
		{

			IAuthenticator authenticator = principal != null
											   ? GetDefaultAuthenticator(principal.AccessToken)
											   : null;
			return ExecuteAsync<T>(HttpMethod.Post, uri, parameters, authenticator);
		}

		/// <summary>
		/// Posts asynchronously.
		/// </summary>
		/// <param name="uri">The URI.</param>
		/// <param name="principal">The principal.</param>
		/// <param name="parameters">The parameters.</param>
		/// <returns>Task.</returns>
		protected Task<IBaseEnvelope<Object>> PostAsync(String uri, Principal principal = null, Object parameters = null)
		{
			return PostAsync<Object>(uri, principal, parameters);
		}

		/// <summary>
		/// Puts asynchronously.
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="uri">The URI.</param>
		/// <param name="principal">The principal.</param>
		/// <param name="parameters">The parameters.</param>
		/// <returns>Task{``0}.</returns>
		protected Task<IBaseEnvelope<T>> PutAsync<T>(String uri, Principal principal = null, Object parameters = null)
			where T : class
		{
			IAuthenticator authenticator = principal != null
											   ? GetDefaultAuthenticator(principal.AccessToken)
											   : null;
			return ExecuteAsync<T>(HttpMethod.Put, uri, parameters, authenticator: authenticator);
		}

		/// <summary>
		/// Deletes asynchronously.
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="uri">The URI.</param>
		/// <param name="principal">The principal.</param>
		/// <param name="parameters">The parameters.</param>
		/// <returns>Task{``0}.</returns>
		protected Task<IBaseEnvelope<T>> DeleteAsync<T>(String uri, Principal principal = null, Object parameters = null)
			where T : class
		{
			IAuthenticator authenticator = principal != null
											   ? GetDefaultAuthenticator(principal.AccessToken)
											   : null;
			return ExecuteAsync<T>(HttpMethod.Delete, uri, parameters, authenticator: authenticator);
		}

		/// <summary>
		/// Deletes asynchronously.
		/// </summary>
		/// <param name="uri">The URI.</param>
		/// <param name="principal">The principal.</param>
		/// <param name="parameters">The parameters.</param>
		/// <returns>Task.</returns>
		protected Task<IBaseEnvelope<Object>> DeleteAsync(String uri, Principal principal = null,
																Object parameters = null)
		{
			return DeleteAsync<Object>(uri, principal, parameters);
		}

		/// <summary>
		/// Sends the Heads asynchronously.
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="uri">The URI.</param>
		/// <param name="principal">The principal.</param>
		/// <param name="parameters">The parameters.</param>
		/// <returns>Task{``0}.</returns>
		protected Task<IBaseEnvelope<T>> HeadAsync<T>(String uri, Principal principal = null, Object parameters = null)
			where T : class
		{
			IAuthenticator authenticator = principal != null
											   ? GetDefaultAuthenticator(principal.AccessToken)
											   : null;
			return ExecuteAsync<T>(HttpMethod.Head, uri, parameters, authenticator: authenticator);
		}

		#endregion

		/// <summary>
		/// Executes the method asynchronously.
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="method">The method.</param>
		/// <param name="uri">The URI.</param>
		/// <param name="parameters">The parameters.</param>
		/// <param name="timeout">The timeout.</param>
		/// <param name="authenticator">The authenticator.</param>
		/// <param name="deserializer">The deserializer.</param>
		/// <param name="serializer">The serializer.</param>
		/// <param name="responseErrorHandler">The response error handler.</param>
		/// <param name="uploadFile">File to upload</param>
		/// <returns>Task{``0}.</returns>
		protected Task<IBaseEnvelope<T>> ExecuteAsync<T>(
			HttpMethod method,
			String uri,
			Object parameters,
			IAuthenticator authenticator,
			IResponseErrorHandler responseErrorHandler = null) where T : class
		{
			return Task.Run(async () =>
				{
					var returnValue = default(IBaseEnvelope<T>);
					var tryAgain = true;
					var counter = 0;
					var response = default(HttpResponseMessage);

					try
					{
						var httpObj = new HttpRequestObject(authenticator, QueryStringSerializer, Serializer);

						while (tryAgain && counter++ < 3)
						{
							try
							{
								response = await httpObj.ExecuteRequestAsync<T>(_httpClient, method, Configuration.BaseUri, uri, parameters);
								tryAgain = false;
							}
							catch (ContractMeow.Api.Exceptions.RateLimitExceededException)
							{
								Debug.WriteLine("Rate Limit Exceeded");
							}
							if (tryAgain)
							{
								await Task.Delay(10000);//wait 10sec before trying again
							}
						}
						if (!tryAgain)
						{
							var responseHandler = new HttpResponseHandler(Deserializer, ResponseErrorHandler);
							returnValue = await responseHandler.HandleResponseAsync<T>(response);
							response.Dispose();
						}
					}
					catch (AggregateException ae)
					{
						ae.Flatten().Handle(e =>
							{
								if (e.GetType() != typeof(ContractMeow.Api.Exceptions.RateLimitExceededException))
								{
									returnValue = new BaseEnvelope<T>() { Exception = (e.InnerException ?? e) };
								}
								return true;
							});
					}
					catch (HttpRequestException ex)
					{
						if (ex.InnerException is WebException)
						{
							var webEx = ex.InnerException as WebException;
							var status = webEx.Status;

							if (status.ToString() == "NameResolutionFailure")
							{
								//this is offline case
								var offlineException = new OfflineException();
								returnValue = new BaseEnvelope<T>() { Exception = offlineException };
							}
						}
					}
					catch (Exception ex)
					{
						returnValue = new BaseEnvelope<T>() { Exception = ex };
					}
					return returnValue;
				});
		}

		/// <summary>
		/// Verifies if the credentials are valid.
		/// </summary>
		/// <param name="credentials">The credentials.</param>
		/// <exception cref="System.Exception">Client is not logged in.</exception>
		protected void VerifyCredentials(Principal credentials)
		{
			if (credentials != null)
			{
				return;
			}

			throw new Exception("Client is not logged in.");
		}

		/// <summary>
		/// Gets the default authenticator.
		/// </summary>
		/// <param name="accessToken">The access token.</param>
		/// <returns>IAuthenticator.</returns>
		private IAuthenticator GetDefaultAuthenticator(String accessToken)
		{
			if (String.IsNullOrWhiteSpace(accessToken))
				return null;

			return new BearerTokenAuthenticator(accessToken);
		}

		#region Disposable

		~ClientBase()
		{
			Dispose(false);
		}

		public void Dispose()
		{
			Dispose(true);
		}

		/// <summary>
		/// Releases unmanaged and - optionally - managed resources. 
		/// </summary>
		/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
		protected virtual void Dispose(bool disposing)
		{
			if (_disposed)
			{
				return;
			}

			if (!disposing)
			{
				if (_httpClient != null)
				{
					_httpClient.Dispose();
					_httpClient = null;
				}
				if (_httpClientHandler != null)
				{
					_httpClientHandler.Dispose();
					_httpClientHandler = null;
				}
				GC.SuppressFinalize(this);
			}
			//no unmanaged resources to release
			_disposed = true;
		}
		#endregion
	}
}
